/**
 * 原型功能扩展
 */
 Date.prototype.format = function(pattern) {
    const format = { "y+": this.getFullYear(), "M+": this.getMonth() + 1, "d+": this.getDate(), "H+": this.getHours(), "m+": this.getMinutes(), "s+": this.getSeconds(), "S": this.getMilliseconds(), "h+": (this.getHours() % 12), "a": (this.getHours() / 12) <= 1 ? 'AM' : 'PM' };
    for (let key in format) {
        const regExp = new RegExp('(' + key + ')');
        const match = pattern.match(regExp);
        if (!match) 
            continue;
        let zero = "";
        for (let i = 0; i < match[1].length; i++) zero += "0";
        const replacement = match[1].length == 1 ? format[key] : (zero + format[key]).substring((('' + format[key]).length));
        pattern = pattern.replace(match[1], replacement);
    }
    return pattern;
}

/**
 * 模块化引入
 */
const { require, lazyRequire, define, getUpperLevelURL } = (() => {
    const moduleLoadTrigger = [];
    const moduleLoadStatus = {};
    const moduleCache = {};

    /**
     * 获取调用者上N级URL
     * 
     * @param {Number} level 上跳级数
     * @return {String} URL
     */
    function getUpperLevelURL(level = 2) {
        const stackArray = new Error().stack.split("\n");  //获取调用栈
        //如果是懒引入则需要再上跳一级
        if(/lazyRequire/.test(stackArray[level]))
            level++;
        let temp = stackArray[level];
        const match = temp.match(/at (.+)\/.+:\d+:\d+/) || temp.match(/@(.+)\/.+:\d+:\d+/);  //正则匹配得到URL
        return match[1] + "/";
    }

    /**
     * 引入模块
     * 
     * 通过此函数可使用相对路径的方式引入需要的模块
     * 
     * @param  {...String[]} paths 模块相对路径
     * @return {Any} 模块暴露值
     */
    async function require(...paths) {
        let upperLevelUrl = getUpperLevelURL(3);  //获得调用者脚本所在基础URL
        const promises = [];
        paths.forEach(path => {
            //如果未已.js结尾则自动拼接后缀名
            if(!/\.js(\?.+)?$/.test(path))
                path += ".js";
            path = upperLevelUrl + path;  //将获得的基础URL拼接指定路径获得脚本访问URL
            const scriptDom = document.createElement("script");  //创建script元素
            scriptDom.setAttribute("src", path);  //设置script元素src让浏览器自动加载脚本
            const url = scriptDom.src;  //获得脚本真正意义上的URL
            //如果当前URL的模块还在加载中则将它加入等待队列中，等待模块加载完成再通知
            if(moduleLoadStatus[url])
                return promises.push(new Promise(resolve => {
                    if(!_.isArray(moduleLoadTrigger[url]))
                        moduleLoadTrigger[url] = [];
                    moduleLoadTrigger[url].push(resolve);
                }));
            moduleLoadStatus[url] = true;  //设置对应URL模块加载状态为正在加载
            promises.push(new Promise((resolve, reject) => {
                //如果存在模块缓存则直接返回缓存
                if(moduleCache[url]) {
                    delete moduleLoadStatus[url];
                    return resolve(moduleCache[url]);
                }
                //监听script元素脚本加载完成事件
                scriptDom.onload = () => {
                    (async () => {
                        //检查模块是否暴露值，如未暴露则无法加载
                        if(!moduleCache[url])
                            throw new Error("module is not export value");
                        //如果模块define阶段错误则将错误抛出到require调用者
                        if(_.isError(moduleCache[url]))
                            throw moduleCache[url];
                        //如果模块暴露值是Promise则等待值
                        let module;
                        if(Object.prototype.toString.call(moduleCache[url]) === "[object Promise]")
                            module = await moduleCache[url];
                        else
                            module = moduleCache[url];
                        (moduleLoadTrigger[url] || []).forEach(trigger => trigger(module));  //将同URL的模块加载队列进行逐一触发让他们返回模块值
                        delete moduleLoadStatus[url];  //解除模块加载状态
                        delete moduleLoadTrigger[url];  //解除模块的等待队列
                        return module;  //返回模块暴露值
                    })()
                    .then(module => resolve(module))
                    .catch(err => {
                        delete moduleLoadStatus[url];  //解除模块加载状态
                        delete moduleLoadTrigger[url];  //解除模块的等待队列
                        reject(err);
                    });
                };
                //监听script元素脚本加载错误事件
                scriptDom.onerror = err => {
                    delete moduleLoadStatus[url];  //解除模块加载状态
                    delete moduleLoadTrigger[url];  //解除模块的等待队列
                    reject(err);
                };
                document.head.appendChild(scriptDom);  //将script元素添加到head元素中正式生效
            }));
        });
        return promises.length > 1 ? Promise.all(promises) : promises[0];  //当只引入一个模块将返回模块如果是一组模块将返回一个数组
    }

    /**
     * 懒引入模块
     * 
     * 此函数主要用于辅助解决模块互相引用造成的冲突问题
     * 
     * @param {String} path 
     * @param {Function} callback 
     */
    function lazyRequire(path, callback) {
        require(path)
        .then(callback)
        .catch(err => console.error(`module ${path} lazy require error:`, err));
    }

    /**
     * 定义模块
     * 
     * 此函数将把模块代码包含其中避免污染全局
     * 同时接收module内return的值作为模块暴露值
     * 
     * @param {Function} module 模块定义函数
     */
    function define(module = () => {}) {
        try {
            moduleCache[document.currentScript.src] = module();  //将模块定义后的结果加入到模块缓存
        }
        catch(err) {
            moduleCache[document.currentScript.src] = err;  //如发生错误则将错误加入模块缓存等待进一步处理
        }
    }

    return { require, lazyRequire, define };  //将函数暴露到全局
})();

/**
 * 日志打印
 * 
 * 对原有的控制台输出进行了封装
 * 主要实现输出时附带时间点、调用者位置、日志等级
 */
const logger = (() => {
    function getStackTopCodeLocation(level = 3) {
        const stackArray = ((new Error()).stack).replace(/\s{4}at./g, '').split('\n');
        if(stackArray.length == 0 || !stackArray[level + 1])
            return null;
        let scriptPath, codeLine, codeColumn, match;
        if((match = stackArray[level + 1].match(/^(.+):(\d+):(\d+)$/)))
            ([, scriptPath, codeLine, codeColumn] = match);
        else if((match = stackArray[level + 1].match(/^(.+)\s\((.+):(\d+):(\d+)\)$/)))
            ([, , scriptPath, codeLine, codeColumn] = match);
        else if((match = stackArray[level + 1].match(/(.+):(\d+):(\d+)\)$/)))
            ([, scriptPath, codeLine, codeColumn] = match);
        if(!codeLine || !codeColumn) return null;
        let scriptName = "unknown";
        if(scriptPath) {
            const match = scriptPath.match(/([\w_\-]+).js$/);
            if(match && match[1]) scriptName = match[1];
        }
        return { scriptName, codeLine: parseInt(codeLine), codeColumn: parseInt(codeColumn) };
    }
    function generateLogHead(level) {
        const { scriptName, codeLine, codeColumn } = getStackTopCodeLocation();
        return `[${new Date().format("yyyy-MM-dd HH:mm:ss.S")}][${level}][${scriptName}<${codeLine || "?"},${codeColumn || "?"}>]`;
    }
    return {
        log: (...args) => console.log(generateLogHead("LOG"), ...args),
        info: (...args) => console.info(`%c${generateLogHead("INFO")}`, "color:#0091ea", ...args),
        warn: (...args) => console.warn(generateLogHead("WARNING"), ...args),
        error: (...args) => console.error(generateLogHead("ERROR"), ...args)
    };
})();
